package nl.windgazer.arachne.components;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import nl.windgazer.arachne.mixins.CKEditor;
import nl.windgazer.arachne.services.ArachnePrivileges;
import nl.windgazer.arachne.services.ContentManagerService;
import nl.windgazer.arachne.services.externalsource.ManagedContent;
import nl.windgazer.arachne.util.ArachneSymbolConstants;
import nl.windgazer.arachne.util.HtmlHeadBlocks;

import org.apache.log4j.Logger;
import org.apache.tapestry5.Asset;
import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.EventConstants;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.annotations.BeforeRenderTemplate;
import org.apache.tapestry5.annotations.BeginRender;
import org.apache.tapestry5.annotations.CleanupRender;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.MixinClasses;
import org.apache.tapestry5.annotations.OnEvent;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Path;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.annotations.SetupRender;
import org.apache.tapestry5.annotations.SupportsInformalParameters;
import org.apache.tapestry5.corelib.components.BeanEditForm;
import org.apache.tapestry5.corelib.components.TextArea;
import org.apache.tapestry5.dom.Element;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.services.SymbolSource;
import org.apache.tapestry5.json.JSONLiteral;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.PageRenderLinkSource;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.javascript.InitializationPriority;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
import org.apache.tapestry5.services.javascript.StylesheetLink;
import org.apache.tapestry5.services.javascript.StylesheetOptions;

/**
 * This is a component that will grab content from an external source (such as a CMS) and renders it as 'raw' text.
 * In order to define the content correctly this component generates the path to the current page and requests
 * a 'uid' to signify which text for this page to render. A call to a 'service' layer implementation is provided
 * with the path and the uid and expected to return a valid block of html.
 * The service layer is expected to return a <code>ManagedContent</code>, which may also contain additional css and/or
 * javascript. Both of which will be rendered in the 'head' of the page. It is the task of a content auditor to asure
 * these styles and/or scripts do not interfere with the rest of the page.
 * 
 * This Component renders as the original html-tag from the template or 'div' if it was using its' T5 tagname.
 * 
 * Parameters:
 * <dl>
 * <dt>uid</dt>
 * <dd>The unique id for one specific block of text on the page, this is combined with the path to this page to get
 * a specific block of html.</dd>
 * </dl>
 * 
 * @author mreuring
 */

@Import(stylesheet="ExternalSource.css")
@SupportsInformalParameters
public class ExternalSource {

	private final static Logger logger = Logger.getLogger(ExternalSource.class);

	private final static String EDITOR_SUFFIX = "_EDITOR";

	private final static String REQUEST_PARAM_PATH = "calculatedPath";

	private final static String REQUEST_PARAM_UID = "calculatedUID";

	//Injects and Persists

	@Property
	@Inject
	private ComponentResources _cr;

	@Inject
	private PageRenderLinkSource _prls;

	@Inject
	private ContentManagerService _cms;

    @Inject
    @Path("ExternalSource.js")
    private Asset _esjs;

    @Inject
    @Path("../mixins/livepipe.js")
    private Asset _livepipe;

    @Inject
    @Path("../mixins/hotkey.js")
    private Asset _hotkey;

    @Inject
    @Path("../mixins/MessagesInterceptorTranslator.js")
    private Asset _mit;

    @Inject
    @Path("classpath:/nl/windgazer/jsrepository/Events.js")
    private Asset _events;

    @Inject
    @Path("classpath:/nl/windgazer/jsrepository/LinkListener.js")
    private Asset _linklistener;

	@Inject
	private JavaScriptSupport _jss;

	@Inject
	private SymbolSource _symbols;

	@Inject
	private Request _request;

	@Inject
	private ArachnePrivileges _ap;

	@Inject
	private Messages _msgs;

	@Inject
	@Symbol(ArachneSymbolConstants.TRANSLATOR_ROLE)
	private String trans_role;

	@Inject
	@Symbol(ArachneSymbolConstants.TRANSLATION_ENABLED)
	private boolean trans_enabled;

	@Inject
	@Symbol(ArachneSymbolConstants.EC_ALTERNATE_NAMING)
	private boolean alternateNaming;

	//Properties and Parameters

	private String id = null;

	private boolean notranslation = true;

	private static final String DEFAULT_ELEMENT = "div";

//	private static final Pattern pattern = Pattern.compile("(<a[^>]* href=\")t5link:([^\"]+)(\"[^>]*>)");
	private static final Pattern pattern2 = Pattern.compile("(<[^>]+ (src|href)=\")t5(link|context):([^\"]+)(\"[^>]*>)");// 1 is start, 2 is src/href, 3 is link/context, 4 is path, 5 is end

	/**
	 * The unique id for one specific block of text on the page, this is combined with the path to this page to get
	 * a specific block of html.
	 */
	@Parameter(defaultPrefix=BindingConstants.LITERAL,required=true)
	@Property
	private String uid;

	@Parameter(defaultPrefix=BindingConstants.LITERAL, required=false)
	private String path = null;

	/**
	 * Setting this parameter to true will cause the editor to render with the 'informal' element instead of forcing a div. 
	 * This will also cause the behaviour of CKEditor to change, no longer forcing a 'p' element on inline-elements, 
	 * instead allowing the user more freedom. <br />
	 * This setting has a greater risk of causing 'invalid' html on the page!!
	 */
	@Parameter(required = false, principal = false, defaultPrefix = BindingConstants.PROP, name = "expert")
	private boolean _expert = false;

	@Property
	private ManagedContent content = null;

	@Property(read=true,write=true)
	private JSONObject _config = new JSONObject(
			"width", "590px"
			, "height", "200px"
			, "toolbar", "Medium"
	);

	//Components

	@SuppressWarnings("unused")
	@Component(id = "sourceEditor", parameters = {"object=content","add=control"})
	private BeanEditForm _sourceEditorComponent;

	@SuppressWarnings("unused")
	@Component(id = "contentEditor", parameters = {"config=prop:config"})
	@MixinClasses(value=CKEditor.class)
	private TextArea _contentEditorComponent;

	//Events

	/*
	 * Using hidden fields in the form and PREPARE_FOR_... events to capture the required information
	 * inside the form. This method is used to allow multiple editors to exist within a loop but without
	 * an external form.
	 * This component cannot, as yet, be used inside a form...
	 */
	
	@Property
	private String calculatedPath;
	@Property
	private String calculatedUID;

	/**
	 * Before Prepare, but only on form-submit...
	 * Grab the path and uid from 'request'
	 */
	@OnEvent(value=EventConstants.PREPARE_FOR_SUBMIT)
	void setupContent() {
		this.calculatedPath = _request.getParameter(REQUEST_PARAM_PATH);
		this.calculatedUID = _request.getParameter(REQUEST_PARAM_UID);
		logger.debug("Setting external-source parameters based on request parameters (submit phase): " + this.calculatedPath + "/" + this.calculatedUID);
	}

	/**
	 * Before Prepare, but only on render...
	 * Grab path and uid from 't5params'
	 */
	@OnEvent(value = EventConstants.PREPARE_FOR_RENDER)
	void setupParamsForRender() {
		this.calculatedPath = getPagePath();
		this.calculatedUID = uid;
		logger.debug("Setting external-source parameters based on T5 parameters (render phase): " + this.calculatedPath + "/" + this.calculatedUID);
	}

	/**
	 * Use internal properties to fetch content...
	 */
	@OnEvent(component="sourceEditor",value=EventConstants.PREPARE)
	void prepContentParameter() {
		logger.debug("Attempting to set the content parameter.");
		if (content == null) content = _cms.getContent(calculatedPath, calculatedUID);
	}

	/**
	 * Renders the original html-element, or a div if the original element was a T5 reference tag. Also includes the
	 * rendering of all html attributes (such as class, id, rel...)
	 */
	@SetupRender
	void renderInformalsEtc(MarkupWriter writer) {
		content = _cms.getContent(getPagePath(), uid);
		//Element informal = writer.element(_cr.getElementName(DEFAULT_ELEMENT)); //Create an element with the found element-name, or if that was a component, use default (div).
		//I prefer above commented method of using the 'actual' informal element-name, but that has too high a risk of generating 'corrupt' html-code, which will break the editor if not the entire page...
		Element informal = writer.element(_expert?_cr.getElementName(DEFAULT_ELEMENT):DEFAULT_ELEMENT); //If expert is set to true, use potentially unsafe element name, other-wise go for div...
		if (_expert) {
			//_jss.addScript("CKEDITOR.config.enterMode = CKEDITOR.ENTER_BR;CKEDITOR.config.shiftEnterMode = CKEDITOR.ENTER_P;");
			_config.put("enterMode", new JSONLiteral("CKEDITOR.ENTER_BR"));
			_config.put("shiftEnterMode", new JSONLiteral("CKEDITOR.ENTER_P"));
		}
	     _cr.renderInformalParameters(writer); //Render informal parameters defined on the element in the template...
	     informal.addClassName("externalSourceElement");
	     if (content.getContent().length() < 1) informal.addClassName("emptyExternalSourceElement");
	    //finishElementEtc(writer);
	}

	@BeginRender
	boolean verifyRender() {
		//Here we should check if there is a good reason not to render the template (for instance, privileges)
		if (content!=null) {
			//final String ckdir = _basedir.getResource().toURL().toString();
			if (trans_enabled && _ap.hasRole(trans_role)) {
				if (_cms.canStoreContent()) {
					_jss.importJavaScriptLibrary(_livepipe);
					_jss.importJavaScriptLibrary(_hotkey);
					_jss.importJavaScriptLibrary(_events);
					_jss.importJavaScriptLibrary(_linklistener);
					_jss.importJavaScriptLibrary(_mit);
					_jss.importJavaScriptLibrary(_esjs);
					_jss.addInitializerCall(InitializationPriority.LATE, "createNewExternalSource", getCID());
					_jss.addInitializerCall(InitializationPriority.LATE, "enableTranslatorHotkey", "");
					notranslation  = false;
			        logger.debug("Rendering an editor for the External Source.");
			        return true;
				} else {
					logger.debug("Cancelling rendering of the editor, ContentManager does not support storing new translations.");
				}
			} else {
				logger.debug("Cancelling rendering of the editor, no role available or correct one not set.");
			}
			return true;
		} else {
			logger.debug("Cancelling rendering of content, content == null");
		}
		return false;
	}

	@BeforeRenderTemplate
	boolean renderContent(MarkupWriter writer) {
	    renderExternalContent(writer);
	    return !notranslation;
	}

	/**
	 * Closing the code-generated html-element.
	 * @param writer
	 */
	@CleanupRender
	void finishElementEtc(MarkupWriter writer) {
		writer.end();//Close the generated element
		id = null;
	}

	@OnEvent(component="sourceEditor",value=EventConstants.SUCCESS)
	void saveContent() {
		if (trans_enabled && _cms.canStoreContent() && _ap.hasRole(trans_role)) {
			_cms.storeContent(content);
		}
	}

	//Other (check for business-logic, it should not remain here)

	/**
	 * Renders the html from an external source. This component calls upon a 'service' layer and provides the path to
	 * the current page and the uid.
	 * @param writer
	 * @return
	 */
	void renderExternalContent(MarkupWriter writer) {

		final String trans_role = _symbols.valueForSymbol(ArachneSymbolConstants.TRANSLATOR_ROLE);
		final StringBuilder sb = new StringBuilder(getPagePath());
		sb.append('[').append(uid).append(']');
		writer.getElement().attribute("rel", sb.toString());

		if (content!=null) {
			final StringBuffer rsb = new StringBuffer();
			String sContent = content.getContent();
			if (sContent.length() > 0) {

				//Find and replace t5link: hrefs
				Matcher matcher = pattern2.matcher(sContent);
				while (matcher.find()) {// 1 is start, 2 is src/href, 3 is link/context, 4 is path, 5 is end
					final StringBuilder t5link = new StringBuilder();
					if (matcher.group(3).equals("link")) {
						t5link.append(_prls.createPageRenderLink(matcher.group(4)).toURI());
					} else {
						//t5link.append(_request.getContextPath());
						t5link.append(_prls.createPageRenderLinkWithContext(_cr.getPage().getClass(), matcher.group(4)));
						//t5link.append(matcher.group(4));
					}
					if (t5link.length()>0) {
						t5link.insert(0, matcher.group(1));
						t5link.append(matcher.group(5));
						matcher.appendReplacement(rsb, t5link.toString());
					}
				} sContent = matcher.appendTail(rsb).toString();

				writer.writeRaw(sContent);
			} else if(_ap.hasRole(trans_role)) writer.writeRaw(_msgs.format("externalsource.nocontent", content.getUID()));
			if (content.getScriptBlock()!=null) {
				HtmlHeadBlocks.writeScriptBlock(content.getScriptBlock(), writer.getDocument());
			}
			if (content.getStyleBlock()!=null) {
				HtmlHeadBlocks.writeStyleBlock(content.getStyleBlock(), writer.getDocument());
			}
			if (content.getScripts()!=null) {
				for (String script:content.getScripts()) {
					_jss.importJavaScriptLibrary(content.getBaseURL() + script);
				}
			}
			if (content.getStyles()!=null) {
				for (String script:content.getStyles()) {
					_jss.importStylesheet(new StylesheetLink(content.getBaseURL() + script, new StylesheetOptions("screen,print")));  //importStylesheet(content.getBaseURL() + script,"screen,print"");
				}
			}
		}
	}

	/**
	 * The logic required for rendering the path to this page. Wrapped in a method so that we can easily
	 * re-use the logic.
	 */
	public String getPagePath() {
		if (path==null) {
			if (alternateNaming) {
				path = "/" + _cr.getPageName();
			} else {
				path = _prls.createPageRenderLink(_cr.getPage().getClass()).getBasePath();
			}
		}
		logger.debug(path);
		return path;
	}

	public String getId() {
		if (id  == null) {
			id = _jss.allocateClientId(_cr.getId());
		}
		return id;
	}

	public String getCID() {
		return new StringBuffer(getId()).append(EDITOR_SUFFIX).toString();
	}

}
